
/*PROBLEMS
1) ALLOCATE MATRIX NEEDS TO GET ***INT like int test
2) what happens if allocation in thread fails? maybe should allocate blocks in main
3) we need to change the second lock to be of "error checking" type
*/

/*TODO*/
/*

who?|done?| function name:
ela	| yes |	int mulMatrix(Mat mat1, Mat mat2, Mat matRes, int N_Off, int M_Off, int O_Off, int nSize, int mSize, int oSize);
ela	| yes |	void parser(Mat matA, Mat matB, int N, int M, int O);
ela	| yes |	void printMatrix (Mat mat, int rows, int cols);

ela	| no  |	void updateMatrix (Job* job, Mat blockToAdd, Mat MatC);
ela	| no  | _threadRunWrapper

jonathan	| yes  |	void divideToJobs(int N, int M, int O, int numOfThreads, Queue queue);
jonathan	| yes  |	void doJob (Job* job, Mat matA, Mat matB, Mat matC);
jonathan	| yes  |	void threadRun (Queue queue, Mat matA, Mat matB, Mat matC) (need to finish)

	
	| no  |  main and allocate function (need to finish)
	| no  |	_freeAll
	| no  |	int getBlockID(Job job);

*/


#include "matmul.h"
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int _allocAll(Mat *matA, Mat *matB, Mat *matC, int N, int M, int O,
		pthread_t** threadArray, int threadNum);
void _freeAll(Mat matA, Mat matB, Mat matC, int N, int M, int O, int blocksRows,
		 pthread_t* threadArray);
void printOutput(Mat matA, Mat matB, Mat matC, int N, int M, int O);
void scanOneMatrix(Mat mat, int rows, int cols);
void printMatrix (Mat mat, int rows, int cols, FILE* stream);
void * _threadRunWrapper(void * threadRunWrapperArgs);
int _roof(float value);
Job _dequeueJob(Queue jobQueue);
void _enqueueJob(Queue jobQueue, Job job);
void _freeMatrix(Mat mat, int rows);

static MutexMatrix locks = NULL; //TODO: I don't think the initialization here is legal -Ella
static 	Queue jobQueue;

int main(int argc, char ** argv) {
	assert(argc == 5);
        pthread_mutexattr_t attr;
	pthread_mutexattr_init(&attr);
        
        if ((pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK_NP)) != 0){
		return 1;
	}
        
       
         
         
        //a to i argv
	int N, M, O, threadNum;
	N = atoi(argv[1]);
	M = atoi(argv[2]);
	O = atoi(argv[3]);
	threadNum = atoi(argv[4]);

	Mat matA = NULL, matB = NULL, matC = NULL;
	pthread_t* threadArray = NULL;

        
        printf("let's go!\n");
        
        
	int retVal = _allocAll(&matA, &matB, &matC, N, M, O,  &threadArray,
			threadNum);
	if (retVal != SUCCESS) {
		return 1;
	}


	int i = 0, j = 0;
        
        int tileSize = (N/threadNum > 0)? (N/threadNum) : 1;
	int blocksRows = _roof((double)N/tileSize);
	int blocksCols = _roof((double)O/tileSize);
	int blocksMids = _roof((double)M/tileSize);
	
	//call parser
	parser(matA, matB, N, M, O);

        printf("after parser\n");
	//create queue (with lock)
	jobQueue.size = blocksRows * blocksCols * blocksMids; 
	jobQueue.iterator = 0;
        if ((pthread_mutex_init(&(jobQueue.queueMutex), &attr)) != 0) {
            _freeAll(matA, matB, matC, N, M, O, blocksRows, threadArray);
            return 1;
        }
        
        //Init lock_matrix
	for (i = 0; i < blocksRows; ++i) {
		for (j = 0; j < blocksCols; ++j) {
			if ((pthread_mutex_init(&(locks[i][j]), &attr)) != 0) {
				_freeAll(matA, matB, matC, N, M, O, blocksRows, 
						threadArray);
				return 1;
			}
		}
	}

        printf("before divide to jobs\n");
	//call function devideToJobs (fills queue)
	divideToJobs(N, M, O, threadNum); //TODO: make sure no allocation problem


        printf("after divide to jobs\n");
	//creates threads
	//TODO: allocate thread_t array in allocAll call it threadArray

	ArgStruct threadRunWrapperArgs;
	threadRunWrapperArgs.queue = jobQueue;
	threadRunWrapperArgs.matA = matA;
	threadRunWrapperArgs.matB = matB;
	threadRunWrapperArgs.matC = matC;
	threadRunWrapperArgs.threadId = 0;
        

	for (i = 0; i < threadNum; ++i) {
            threadRunWrapperArgs.threadId = i;
            int res = pthread_create(threadArray + i, NULL, _threadRunWrapper,
                            (void*) (&threadRunWrapperArgs)); //TODO: make sure about pthread_attr_t being NULL
            if (res != 0) {
                    _freeAll(matA, matB, matC, N, M, O, blocksRows, 
                                    threadArray);
                    return 1; //TODO: verify returning terminates all threads
            }
	}
	//TODO:	 PROBLEM WITH ALLOCATION BLOCK IN SPECIFIC THREAD, WHAT HAPPENS? HOW TO WE FREE ALL? OR MAYBE ONLY KILL THREAD?

	//join threads
	for (i = 0; i < threadNum; ++i) {
		int res = pthread_join(threadArray[i], NULL); //TODO: make sure about pthread_attr_t being NULL
		if (res != 0) {
			_freeAll(matA, matB, matC, N, M, O, blocksRows, 
					threadArray);
			return 1; //TODO: verify returning terminates all threads
		}
	}

	/*each son takes jobs from queue (locking and unlocking) and does it.
	 sleeps if queue is already locked, Exits when no more jobs.
	 (at end of job updates matC)*/

	//call printOutput
	printOutput(matA, matB, matC, N, M, O);
	_freeAll(matA, matB, matC, N, M, O, blocksRows,  threadArray);
	return 0;
}

void _freeAll(Mat matA, Mat matB, Mat matC, int N, int M, int O, int blocksRows,
		 pthread_t* threadArray) {
	int i = 0;
	for (i = 0; i < N; ++i) {
		free(matA[i]);
		free(matC[i]);
        }
	for (i = 0; i < M; ++i) {
		free(matB[i]);
	}
	//Allocate locks arrays
	for (i = 0; i < blocksRows; ++i) {
		free(locks[i]);
	}

	free(matA);
	free(matB);
	free(matC);
	free(threadArray);
	free(jobQueue.jobs);
	free(locks);
}

BlockID getBlockID(Job job) {
	BlockID block;
	block.IDRow = job.N_Offset / job.dimentions.n;
	block.IDCol = job.N_Offset / job.dimentions.n;
	return block;
}

void updateMatrix (Job job, Mat blockToAdd, Mat matC) {

		BlockID blockLock = getBlockID(job); 
		pthread_mutex_lock(&(locks[blockLock.IDRow][blockLock.IDCol]));

		int i,j;
		int n=job.dimentions.n;
		int o=job.dimentions.o;
		for (i=0 ; i < n ; ++i) {
			for (j=0 ; j < o ; ++j) {
				matC[job.N_Offset+i][job.O_Offset+j]+=blockToAdd[i][j];
			}
		}
		pthread_mutex_unlock(&(locks[blockLock.IDRow][blockLock.IDCol]));
}





void scanOneMatrix(Mat mat, int rows, int cols) {// TODO: do we need to check input? check if scan fails?
	int i, j;
	for (i=0 ; i<rows ; ++i) {
		for (j=0 ; j<cols ; ++j) {
			scanf("%d", &(mat[i][j]));
		}
	}
}

void parser(Mat matA, Mat matB, int N, int M, int O) { 
//	int i, j;
	scanOneMatrix(matA, N, M);
	scanOneMatrix(matB, M, O);	
}


int mulMatrix(Mat mat1, Mat mat2, Mat matRes, int N_Off, int M_Off, int O_Off, int nSize, int mSize, int oSize) {
// TODO: not sure what we said, but I think matRes is suppose to be m*o, no offset.
// TODO: is matRes inited to zero? make sure it is when allocating. 
	int i, j, k;
	for (i=N_Off ; i < (N_Off + nSize) ; ++i) {
		for (k=O_Off ; k < (O_Off + oSize) ; ++k) {
			for (j=M_Off ; j < (M_Off + mSize) ; ++j) {
				matRes[i-N_Off][k-O_Off]+=(mat1[i][j])*(mat2[j][k]); 	
			}
		}
	}
        return 0;
}

void printMatrix (Mat mat, int rows, int cols, FILE* stream) {
	int i, j;
	for (i=0 ; i < rows ; ++i) {
		for (j=0 ; j < cols ; ++j) {
			fprintf(stream, "%d", (mat[i][j]));
			if (j < (cols-1)) {
				fprintf(stream, "\t");
			}
		}
			fprintf(stream, "\n");
	}
}

void printOutput(Mat matA, Mat matB, Mat matC, int N, int M, int O) { 
	//TODO: is it OK that there is an extra tab at the end of each row and enter at the end?
	printf("Matrix A:\n");
	printMatrix(matA, N, M, stdout);
	printf("Matrix B:\n");
	printMatrix(matB, M, O, stdout);
	printf("Matrix C:\n");
	printMatrix(matC, N, O, stdout);
}

void * _threadRunWrapper(void * threadRunWrapperArgs) {
	threadRun(((ArgStruct*)threadRunWrapperArgs)->queue, ((ArgStruct*)(threadRunWrapperArgs))->matA, 
		((ArgStruct*)(threadRunWrapperArgs))->matB, ((ArgStruct*)(threadRunWrapperArgs))->matC, ((ArgStruct*)(threadRunWrapperArgs))->threadId);
	return NULL;
}

void threadRun (Queue jobQueue, Mat matA, Mat matB, Mat matC, int threadId){
	while(1){
		pthread_mutex_lock(&(jobQueue.queueMutex));
		
                //from here on mutex is held (=locked)
		if (jobQueue.iterator == jobQueue.size){
			pthread_mutex_unlock(&(jobQueue.queueMutex));
			return; //TODO: validate it's OK not to use pthread_exit
		} else{ // =>not empty
		Job job = _dequeueJob(jobQueue); 
		pthread_mutex_unlock(&(jobQueue.queueMutex));
		//from here on mutex is released (=unlocked)
                doJob(job, matA, matB, matC, threadId);
	}
	return ;
    }
}



/*
* Allocate matrixes A  B and C
* inits C to zero and allocate locks array
*/
int _allocAll(Mat *matA, Mat *matB, Mat *matC, int N, int M, int O, 
		pthread_t** threadArray, int threadNum) {
        
        int tileSize = (N/threadNum > 0)? (N/threadNum) : 1;
	int blocksRows = _roof((double)N/tileSize);
	int blocksCols = _roof((double)O/tileSize);
	int blocksMids = _roof((double)M/tileSize);
	int numberOfJobs = blocksRows * blocksCols * blocksMids;
printf( "\ntileSize: %d blockrows: %d block cols: %d mids: %d",tileSize, blocksRows, blocksCols, blocksMids); 
	MatMulResult resA = _allocMatrix(matA, N, M);
	MatMulResult resB = _allocMatrix(matB, M, O);
	MatMulResult resC = _allocMatrix(matC, N, O);
	locks = malloc(blocksRows * sizeof(pthread_mutex_t*));

	
	*threadArray = malloc(threadNum * sizeof(pthread_t));
	
	jobQueue.jobs = NULL;
	jobQueue.jobs = malloc(numberOfJobs * sizeof(Job));
	//TODO: allocate job array inside of jobQueue.
	//TODO: initialize pthread_mutex's in lockArray

	if (resA != SUCCESS || resB != SUCCESS || resC != SUCCESS    
                                || !locks || !(*threadArray)  || !jobQueue.jobs) {
		_freeMatrix(*matA, N);
		_freeMatrix(*matB, M);
		_freeMatrix(*matC, N);
		free(*threadArray);
		free(jobQueue.jobs);
		free(locks);
		return ALLOC_FAILURE;
	}

	int i = 0;  
	//Allocate locks arrays
	for (i = 0; i < blocksRows; ++i) {
		locks[i] = malloc(blocksCols * sizeof(pthread_mutex_t));
	}

	//verify allocation of locks
	for (i = 0; i < blocksRows; ++i) {
		if (!locks[i]) {
			_freeAll(*matA, *matB, *matC, N, M, O, blocksRows, 
					*threadArray);
			return ALLOC_FAILURE;
		}
	}
	return SUCCESS;
}


int countDigits(int n) {
    int count=0;
    while (n != 0) {
        ++count;
        n/=10;
    }
    return count;
}


MatMulResult doJob(Job job, Mat matA, Mat matB, Mat matC, int threadId){
    int blockN = job.dimentions.n;
    int blockM = job.dimentions.m;
    int blockO = job.dimentions.o;
    
    Mat calculatedBlock = NULL;
    MatMulResult res = _allocMatrix(&calculatedBlock,blockN, blockO);
    if (res != SUCCESS)
        return ALLOC_FAILURE;
  
    _initToZeros(calculatedBlock, blockN, blockO);
    mulMatrix(matA, matB, matC, job.N_Offset, job.M_Offset, job.O_Offset, 
                                                        blockN, blockM, blockO);//TODO: causes seg_fault.
    updateMatrix(job, calculatedBlock, matC);
    

    
    char * idString = malloc(countDigits(threadId)+1);
	sprintf(idString,"%d",threadId);
    char * filename= malloc(strlen("mamtul-thread-")+5+strlen(idString));
    
    sprintf(filename, "mamtul-thread-%s.log", idString);
    free(idString);
    FILE* stream = fopen(filename, "w");
    free(filename);
    fprintf(stream, "append_block(%d, %d):/n", job.N_Offset, job.O_Offset);
    printMatrix(calculatedBlock, blockN, blockO, stream);
    fclose(stream);
    _freeMatrix(calculatedBlock, blockN);
    return SUCCESS;
}


/*
 * Description: defines jobs by dividing matrix into blocks, fills queue
 * Input: N, M, O, numOfThreads, empty queue
 * Output: filled queue
 */
void divideToJobs(int N, int M, int O, int numOfThreads){
    int tileSize = (N/numOfThreads > 0)? (N/numOfThreads) : 1;
    
    int i; //N-axis iterator
    int j; //M-axis iterator
    int k; //O-axis iterator
    
    
    for (j = 0;j < M; j += tileSize){
        for (i = 0;i < N; i += tileSize){
            for (k = 0;k < O; k += tileSize){
                Job job;
                Dimentions dim;
                
                dim.n = ((N - i) < tileSize) ? (N-i) : tileSize;
                dim.m = ((M - j) < tileSize) ? (M-j) : tileSize;
                dim.o = ((O - k) < tileSize) ? (O-k) : tileSize;
                
                job.N_Offset = i;
                job.M_Offset = j;
                job.O_Offset = k;
                job.dimentions = dim;
                printf("\nb4 enqueue in divide job %d %d %d\n", i, j, k);
                _enqueueJob(jobQueue, job);
                
                printf("\nafter enqueue in divide job %d %d %d\n", i, j, k);
		
            }
            
        }
        
    }
    return;
}

void print_output(Mat matA, Mat matB, Mat matC, int N, int M, int O){
    return;
}


int _roof(float value) {
	int roofVal, floorVal = value;
	if (value > (float) floorVal) {
		roofVal = floorVal + 1;
	} else {
		roofVal = floorVal;
        }
	return roofVal;
}

Job _dequeueJob(Queue jobQueue){
    assert(jobQueue.iterator > 0); //assumes queue isn't empty.
    return jobQueue.jobs[jobQueue.iterator--];
}

void _enqueueJob(Queue jobQueue, Job job){
  printf("check b4 iterator");
   int it=jobQueue.iterator;
   printf("check b4 size");
  int size=jobQueue.size; 
   printf("check b4 assert");
  
   assert(it < size); //if this fails, we've miscalculated the number of jobs or created too many of them.
    printf("check1 iterator");
    printf("check2 %d", it);
    jobQueue.jobs[it] = job;
    printf("check3");
    jobQueue.iterator++;
    return;
}
//  
void _initToZeros(Mat mat, int rows, int cols){
    int i, j;
    for(i=0 ;i < rows; ++i){
        for(j = 0;j < cols; ++j){
            mat[i][j] = 0;
        }
    }
    return;
}


void _printJob(Queue jobQueue){
    printf( " " );
}

MatMulResult _allocMatrix(Mat *mat, int rows, int cols){
 	void * ptr = malloc(rows*sizeof(int*));
        if (!ptr){
                    return ALLOC_FAILURE;
        }
        *mat = ptr;
        int i;
        //init to null to help with _freeAll
        for(i = 0; i<rows; ++i){
            (*mat)[i] = NULL;
        }
        
        //allocate sub-arrays
        for(i = 0; i<rows; ++i){
		(*mat)[i] = malloc(cols*sizeof(int));
                if (!(*mat)[i]){
                    return ALLOC_FAILURE;
                }
	}
        return SUCCESS;
}


void _freeMatrix(Mat mat, int rows){
    if (!mat){
        return;
    }
    int i;
    for (i = 0; i < rows; ++i) {
                    free(mat[i]);
	}   
    free(mat);
}

